1 /** 2 This module implements a $(LINK2 http://dlang.org/template-mixin.html, 3 template mixin) containing a program to search a list of directories 4 for all .d files therein, then writes a D program to run all unit 5 tests in those files using unit_threaded. The program 6 implemented by this mixin only writes out a D file that itself must be 7 compiled and run. 8 9 To use this as a runnable program, simply mix in and compile: 10 ----- 11 #!/usr/bin/rdmd 12 import unit_threaded; 13 mixin genUtMain; 14 ----- 15 16 Generally however, this code will be used by the gen_ut_main 17 dub configuration via `dub run`. 18 19 By default, genUtMain will look for unit tests in CWD 20 and write a program out to a temporary file. To change 21 the file to write to, use the $(D -f) option. To change what 22 directories to look in, simply pass them in as the remaining 23 command-line arguments. 24 25 The resulting file is also a program that must be compiled and, when 26 run, will run the unit tests found. By default, it will run all 27 tests. To run one test or all tests in a particular package, pass them 28 in as command-line arguments. The $(D -h) option will list all 29 command-line options. 30 31 Examples (assuming the generated file is called $(D ut.d)): 32 ----- 33 rdmd -unittest ut.d # run all tests 34 rdmd -unittest ut.d tests.foo tests.bar # run all tests from these packages 35 rdmd ut.d -h # list command-line options 36 ----- 37 */ 38 39 module unit_threaded.runtime; 40 41 import unit_threaded.from; 42 43 mixin template genUtMain() { 44 45 int main(string[] args) { 46 try { 47 writeUtMainFile(args); 48 return 0; 49 } catch (Exception ex) { 50 import std.stdio : stderr; 51 52 stderr.writeln(ex.msg); 53 return 1; 54 } 55 } 56 } 57 58 struct Options { 59 bool verbose; 60 string fileName; 61 string[] dirs; 62 bool help; 63 bool showVersion; 64 string[] includes; 65 string[] files; 66 67 bool earlyReturn() @safe pure nothrow const { 68 return help || showVersion; 69 } 70 } 71 72 Options getGenUtOptions(string[] args) { 73 import std.getopt; 74 import std.stdio : writeln; 75 76 Options options; 77 auto getOptRes = getopt(args, "verbose|v", "Verbose mode.", &options.verbose, 78 "file|f", "The filename to write. Will use a temporary if not set.", 79 &options.fileName, "I", "Import paths as would be passed to the compiler", 80 &options.includes, "version", "Show version.", &options.showVersion,); 81 82 if (getOptRes.helpWanted) { 83 defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...", 84 getOptRes.options); 85 options.help = true; 86 return options; 87 } 88 89 if (options.showVersion) { 90 writeln("unit_threaded.runtime version v0.6.1"); 91 return options; 92 } 93 94 options.dirs = args.length <= 1 ? ["."] : args[1 .. $]; 95 96 if (options.verbose) { 97 writeln(__FILE__, ": finding all test cases in ", options.dirs); 98 } 99 100 return options; 101 } 102 103 from!"std.file".DirEntry[] findModuleEntries(in Options options) { 104 105 import std.algorithm : splitter, canFind, map, startsWith, filter; 106 import std.array : array, empty; 107 import std.file : DirEntry, isDir, dirEntries, SpanMode; 108 import std.path : dirSeparator, buildNormalizedPath; 109 import std.exception : enforce; 110 111 // dub list of files, don't bother reading the filesystem since 112 // dub has done it already 113 if (!options.files.empty && options.dirs == ["."]) { 114 return dubFilesToAbsPaths(options.fileName, options.files).map!toDirEntry.array; 115 } 116 117 DirEntry[] modules; 118 foreach (dir; options.dirs) { 119 enforce(isDir(dir), dir ~ " is not a directory name"); 120 auto entries = dirEntries(dir, "*.d", SpanMode.depth); 121 auto normalised = entries.map!(a => buildNormalizedPath(a.name)); 122 123 bool isHiddenDir(string p) { 124 return p.startsWith("."); 125 } 126 127 bool anyHiddenDir(string p) { 128 return p.splitter(dirSeparator).canFind!isHiddenDir; 129 } 130 131 modules ~= normalised.filter!(a => !anyHiddenDir(a)).map!toDirEntry.array; 132 } 133 134 return modules; 135 } 136 137 auto toDirEntry(string a) { 138 import std.file : DirEntry; 139 140 return DirEntry(removePackage(a)); 141 } 142 143 // package.d files will show up as foo.bar.package 144 // remove .package from the end 145 string removePackage(string name) { 146 import std.algorithm : endsWith; 147 import std.array : replace; 148 149 enum toRemove = "/package.d"; 150 return name.endsWith(toRemove) ? name.replace(toRemove, "") : name; 151 } 152 153 string[] dubFilesToAbsPaths(in string fileName, in string[] files) { 154 import std.algorithm : filter, map; 155 import std.array : array; 156 import std.path : buildNormalizedPath; 157 158 // dub list of files, don't bother reading the filesystem since 159 // dub has done it already 160 return files.filter!(a => a != fileName).map!(a => removePackage(a)) 161 .map!(a => buildNormalizedPath(a)).array; 162 } 163 164 string[] findModuleNames(in Options options) { 165 import std.path : dirSeparator, stripExtension, absolutePath, relativePath; 166 import std.algorithm : endsWith, startsWith, filter, map; 167 import std.array : replace, array; 168 import std.path : baseName, absolutePath; 169 170 // if a user passes -Isrc and a file is called src/foo/bar.d, 171 // the module name should be foo.bar, not src.foo.bar, 172 // so this function subtracts import path options 173 string relativeToImportDirs(string path) { 174 foreach (string importPath; options.includes) { 175 importPath = relativePath(importPath); 176 if (!importPath.endsWith(dirSeparator)) 177 importPath ~= dirSeparator; 178 if (path.startsWith(importPath)) { 179 return path.replace(importPath, ""); 180 } 181 } 182 183 return path; 184 } 185 186 return findModuleEntries(options).filter!(a => a.baseName != "reggaefile.d") 187 .filter!(a => a.absolutePath != options.fileName.absolutePath).map!(a => relativeToImportDirs(a.name)) 188 .map!(a => replace(a.stripExtension, dirSeparator, ".")).array; 189 } 190 191 string writeUtMainFile(string[] args) { 192 auto options = getGenUtOptions(args); 193 return writeUtMainFile(options); 194 } 195 196 string writeUtMainFile(Options options) { 197 if (options.earlyReturn) { 198 return options.fileName; 199 } 200 201 return writeUtMainFile(options, findModuleNames(options)); 202 } 203 204 private string writeUtMainFile(Options options, in string[] modules) { 205 import std.path : buildPath, dName = dirName; 206 import std.stdio : writeln, File; 207 import std.file : tempDir, getcwd, mkdirRecurse, exists; 208 import std.algorithm : map; 209 import std.array : join; 210 211 if (!options.fileName) { 212 options.fileName = buildPath(tempDir, getcwd[1 .. $], "ut.d"); 213 } 214 215 if (!haveToUpdate(options, modules)) { 216 if (options.verbose) 217 writeln("Not writing to ", options.fileName, ": no changes detected"); 218 return options.fileName; 219 } else { 220 if (options.verbose) 221 writeln("Writing to unit test main file ", options.fileName); 222 } 223 224 const dirName = options.fileName.dName; 225 dirName.exists || mkdirRecurse(dirName); 226 227 auto wfile = File(options.fileName, "w"); 228 wfile.write(modulesDbList(modules)); 229 wfile.writeln(q{ 230 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand. 231 import unit_threaded; 232 }); 233 234 wfile.writeln("int main(string[] args)"); 235 wfile.writeln("{"); 236 237 immutable indent = " "; 238 wfile.writeln(" return args.runTests!(\n" ~ modules.map!(a => indent ~ `"` ~ a ~ `"`) 239 .join(",\n") ~ "\n" ~ indent ~ ");"); 240 wfile.writeln("}"); 241 wfile.close(); 242 243 return options.fileName; 244 } 245 246 private bool haveToUpdate(in Options options, in string[] modules) { 247 import std.file : exists; 248 import std.stdio : File; 249 import std.array : join; 250 import std..string : strip; 251 252 if (!options.fileName.exists) { 253 return true; 254 } 255 256 auto file = File(options.fileName); 257 return file.readln.strip != modulesDbList(modules); 258 } 259 260 //used to not update the file if the file list hasn't changed 261 private string modulesDbList(in string[] modules) @safe pure nothrow { 262 import std.array : join; 263 264 return "//" ~ modules.join(","); 265 }